Skip to content

fix(security): allowlist iframe attributes and source protocols - #21

Merged
asadowski10 merged 2 commits into
developfrom
claude/amazing-clarke-p2ip34
Nov 5, 2026
Merged

asadowski10 merged 2 commits into
developfrom
claude/amazing-clarke-p2ip34

Conversation

@asadowski10

@asadowski10 asadowski10 commented Nov 5, 2026 •

Copy link
Copy Markdown
Member

Summary

Block attributes are controlled by any user able to edit posts, including contributors (no unfiltered_html). The custom iframe attributes were only filtered by a denylist, which allowed stored XSS:

  1. Front end (includes/BlockRenderer.php): any key/value pair in iframeAttributes was rendered. onload, srcdoc, or a crafted key such as x onload=alert(1) y (esc_attr() does not escape spaces or =) were output as-is. WordPress' filter_block_kses_value() does not strip these plain strings.
  2. Editor (src/blockparty-iframe/edit.js / utils.js): srcdoc was passed through to React, and isURL() accepted javascript: URLs. Both executed in the wp-admin origin when an admin opened a contributor's post. This could escalate to an administrator account.

Changes

  • Allowlist of attribute names, shared by PHP and JS: allow, allowfullscreen, allowpaymentrequest, id, referrerpolicy, sandbox, aria-*, data-*. Names must also match ^[a-z][a-z0-9-]*$.
  • Source URL restricted to http / https: esc_url( $url, [ 'http', 'https' ] ) in PHP, and the new isSafeIframeUrl() in the editor (replaces isURL()).
  • Pasted iframe code is now parsed in an inert DOMParser document, and only allowlisted attributes are kept.
  • The deprecated save.js keeps its previous unfiltered output (convertAttributesToRawProps()), so existing posts still pass block validation. That markup is only serialized for comparison, never mounted.
  • CHANGELOG.md: added a Security entry under [Unreleased].

Behaviour change

Attributes outside the allowlist (for example class) are no longer rendered. If a project needs another attribute, add it to both lists (BlockRenderer::ALLOWED_ATTRIBUTES and ALLOWED_ATTRIBUTES in utils.js).

Test plan

  • npx wp-scripts lint-js src: no errors
  • npm run build: compiled successfully
  • php -l includes/BlockRenderer.php
  • Render run with WordPress functions stubbed: onload, srcdoc, a crafted key and class are dropped; allow, allowfullscreen, data-* and aria-* are kept and escaped; a javascript: URL renders an empty wrapper.
  • convertAttributesToProps() / isSafeIframeUrl() checked with Node (javascript: and data: rejected).
  • composer cs (PHPCS): not run locally, because roave/security-advisories conflicts with the local PHP 8.3.28. Left to CI.
  • Manual check in wp-env:
    • As a contributor, insert a block with {"key":"onload","value":"alert(1)"} and {"key":"srcdoc","value":"<script>alert(1)</script>"} via the code editor.
    • Preview it as an admin and reopen it in the editor: no alert should fire.
    • Pasting <iframe src="https://…" allowfullscreen allow="…"> still works.
    • An existing 1.1.x post still validates.

Generated by Claude Code


Note

Medium Risk
Changes security-sensitive iframe rendering and editor validation; merging is correct hardening but drops non-allowlisted attributes (e.g. class) and must stay in sync between PHP and JS.

Overview
Hardens the Blockparty iframe block against stored XSS from contributor-controlled block attributes by switching from a denylist to a shared allowlist in BlockRenderer.php and utils.js (allow, sandbox-related attrs, id, referrerpolicy, plus aria-* / data-* with strict name validation). Event handlers, srcdoc, and malformed attribute names are no longer emitted on the front end or in the editor preview.

Iframe src is now limited to http/https via esc_url() with allowed protocols in PHP and new isSafeIframeUrl() in the editor (replacing isURL()). Pasted iframe HTML is parsed with DOMParser instead of injecting into the live DOM, and only allowlisted attributes are kept.

Deprecated save.js still serializes legacy markup through convertAttributesToRawProps() so older posts validate; live rendering uses the filtered convertAttributesToProps(). CHANGELOG.md documents the security fixes; composer.json bumps the PHP platform pin and WPCS.

Reviewed by Cursor Bugbot for commit ce015a3. Bugbot is set up for automated code reviews on this repo. Configure here.

Block attributes are controlled by any user able to edit posts, including
contributors without unfiltered_html. Custom iframe attributes were only
filtered by a denylist, so event handlers (onload) or srcdoc could be
stored and executed on the front end and in the editor.

- Render only allowlisted attribute names (allow, allowfullscreen,
  allowpaymentrequest, id, referrerpolicy, sandbox, aria-*, data-*) with a
  strict name pattern, in PHP and JS.
- Restrict the iframe source to http/https in PHP and in the editor.
- Parse pasted iframe code in an inert DOMParser document.
- Keep the deprecated save output unfiltered to preserve block validation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ft8SsWmmRXSa4kvxVHVdn
- Bump wp-coding-standards/wpcs to 3.4.1: 3.3.0 is blocked by Composer
  because of security advisory PKSA-mh9b-91zm-m1gy (< 3.4.1).
- Bump config.platform.php to 8.3.36: roave/security-advisories now
  conflicts with PHP 8.3 < 8.3.36.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ft8SsWmmRXSa4kvxVHVdn
@asadowski10
asadowski10 merged commit b33b95f into develop Nov 5, 2026
3 checks passed
@asadowski10
asadowski10 deleted the claude/amazing-clarke-p2ip34 branch November 5, 2026 17:54
@asadowski10 asadowski10 mentioned this pull request Nov 5, 2026
4 of 6 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants